-
Notifications
You must be signed in to change notification settings - Fork 1
[C팀] 백엔드파트 코드리뷰용 PR #76
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클린아키텍처, DDD와 같은 개념이 쉽지 않았을거 같은데, 여러가지 시도를 해주신것 같아서 재미있게 봤습니다. 수고하셨습니다 👍
질문 주신내용중 하나인, 3. 프로젝트의 품질을 향상하고 싶습니다. 에 대해서는 여기에 답해보도록 할게요.
사실 어떤 방향으로의 품질 향상인지가 약간 추상적이라 생각나는대로 적어보겠습니다 🙇♂️
-
이미 알고계실수도 있을거 같은데, AggregateRoot, Domain service, Application Service 에 대해서 공부해보셔도 좋을거 같습니다.
-
클린아키텍처가 과연 좋을까요?
-
테스트작성 방법중에서 Describe-Context-It 이라는 패턴이 있습니다. DDD를 하게 되면 도메인이 비대해지면서 테스트코드도 하나의 클래스에 몰려있는 경우가 발생하는데요. 이때, Describe-Context-It을 쓰면 늘어나는 테스트코드를 관리하기 쉬워집니다.
참고: https://johngrib.github.io/wiki/junit5-nested/ -
sonar cloud를 쓰면 lint를 어느정도 자동화 해서 관리해줍니다. https://www.sonarsource.com/products/sonarcloud/
-
application service를 활용하다보면 모든 로직을 트랜잭션으로 묶지 못하는 경우가 발생하는데요. 이때, 업데이트를 일부만 성공하게 된다면 데이터 정합성은 깨지게 됩니다. 이런 상황을 어떻게 방지할 수 있을까요?
-
각 도메인 끼리는 어떻게 호출을 해야할까요?
-
API가 중복(따닥) 호출되면 어떻게 될까요? 어떻게 멱등한 API를 구현할 수 있을까요?
-
일단 유저를 모으는게 중요한것 같아요. (안된다면 기술을 해야겠지만요) 같은 개선을 하더라도 유저관점에서 유의미한 개선을 했을때 오는 임팩트가 크다고 생각합니다.
더 많을거 같은데 당장 생각나는건 이정도네요 ㅎㅎ
Dockerfile-local
Outdated
|
|
||
| EXPOSE 8080 | ||
| ENTRYPOINT ["java"] | ||
| CMD ["-jar", "backend.jar"] No newline at end of file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
감사합니다!!
build.gradle
Outdated
| dependencies { | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
| // WEB | ||
| implementation 'org.springframework.boot:spring-boot-starter-web' | ||
| compileOnly 'org.projectlombok:lombok' | ||
| developmentOnly 'org.springframework.boot:spring-boot-devtools' | ||
| annotationProcessor 'org.projectlombok:lombok' | ||
|
|
||
| // Testing | ||
| testImplementation 'org.springframework.boot:spring-boot-starter-test' | ||
| testImplementation 'org.springframework.security:spring-security-test' | ||
| testRuntimeOnly 'org.junit.platform:junit-platform-launcher' | ||
| testImplementation 'com.h2database:h2' | ||
|
|
||
| // Development tools (for local development only) | ||
| developmentOnly 'org.springframework.boot:spring-boot-devtools' | ||
|
|
||
| // DB, JPA | ||
| runtimeOnly 'com.mysql:mysql-connector-j' | ||
| implementation 'org.springframework.boot:spring-boot-starter-data-jpa' | ||
|
|
||
| // Lombok (code simplification) | ||
| compileOnly 'org.projectlombok:lombok' | ||
| annotationProcessor 'org.projectlombok:lombok' | ||
|
|
||
| // Validation (for request/response validation) | ||
| implementation 'org.springframework.boot:spring-boot-starter-validation' | ||
|
|
||
| // WebSocket (for real-time communication) | ||
| implementation 'org.springframework.boot:spring-boot-starter-websocket' | ||
|
|
||
| // Swagger-UI | ||
| implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.2' | ||
|
|
||
| // JWT | ||
| implementation 'io.jsonwebtoken:jjwt-api:0.12.6' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' | ||
| runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
마찬가지로 사소한것이지만, 모든 설정을 하나의 gradle파일에 두면, 프로젝트가 커졋을때 관리하기 힘들어집니다.
아래 사진과 같이, 역할별로 gradle파일을 만들고 (메인)build.gradle에 apply를 해주면 관리하기 편해지니 시도해보셔도 좋을거 같습니다.
참고용 레포지토리도 링크로 남겨드립니다
<build.gradle>
apply from: "gradle/db.gradle"
apply from: "gradle/jwt.gradle"
apply from: "gradle/core.gradle"
apply from: "gradle/test.gradle"
apply from: "gradle/slack.gradle"
apply from: "gradle/spring.gradle"
apply from: "gradle/monitor.gradle"
apply from: "gradle/jetbrains.gradle"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
오 적용해보겠습니다! 감사합니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
좋은 팁 감사합니다!!
build.gradle
Outdated
| implementation 'org.springframework.boot:spring-boot-starter-data-redis' | ||
|
|
||
| // WebFlux 외부 API 호출 | ||
| implementation 'org.springframework.boot:spring-boot-starter-webflux' |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
mvc와 webflux를 함께 쓰시나요?
- 이러면 서버는 Tomcat으로 뜨게될까요 Netty로 뜨게될까요?
- mvc와 webflux는 어떤상황에 같이 사용할 수 있을까요?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
현재 저희 서비스는 WebFlux를 활성화하지 않았기에, Tomcat으로 서버가 구동됩니다.
WebFlux를 추가한 이유는 외부 API(OpenAI, ClovaStudio)를 사용하려고 했기에 추가했습니다!
| @Service | ||
| @RequiredArgsConstructor | ||
| @Log4j2 | ||
| @Transactional |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
저도 DDD를 좋아하는데, 저는 트랜잭션 관리의 용이성 때문에 domain Service는 domain레이어에 구현하는걸 추천드립니다. (자연스럽게 @Transactional도 domain 밖으로 빠져나오지 않겠죠?)
application 레이어에서는 여러 domain service 를 조합해서 사용해야 하거나 I/O 작업이 발생하는데, 트랜잭션 안에는 넣으면 안되는 상황에 사용합니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
application service: 여러개의 도메인 서비스의 오케스트레이션 및 트랜잭션분리domain service: 각 도메인 특정한 로직을 담당
| import jakarta.persistence.Enumerated; | ||
| import lombok.*; | ||
|
|
||
| @Embeddable |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클린아키텍처를 사용한것은 아닌가요?
domain layer가 POJO로 되어있고 의존성도 application -> domain <- infra 로 가고있는거 같아서 클린아키텍처 + DDD라고 생각했는데 여기 jakarta쪽 어노테이션이 있군요
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
클린아키텍처를 사용하려고 하였으나, 잘 모르는 상황이었어서 사용하지 않았습니다..!
그대신 표현 -> 응용 -> 도메인 <- 인프라 4계층으로 DDD를 설계하려고 하였습니다!
도메인 계층은 POJO로 구현하고 인프라에서 JPA를 사용하였지만,
밸류 타입의 경우 도메인 계층에서 jakarta 어노테이션을 사용하여 POJO로 생성하지 못했습니다..
밸류 타입도 도메인은 POJO로 변경하고, 인프라에 jakarta 어노테이션 사용 및 mapper를 통해 관리하도록 변경하겠습니다!
감사합니다~
| import spring.backend.activity.query.dao.ActivityDao; | ||
| import spring.backend.activity.query.dao.QuickStartDao; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
member -> activity로 의존성이 가고있네요 ㅎㅎ
DDD를 사용해서 도메인별로 로직을 잘 분리해주어도. 사용하는쪽의 의존성을 관리하지 않으면 관리하기 힘든 코드가 될 수 있습니다! (지금 그렇다는건 아니에요)
다른 도메인들끼리는 어떤 방식으로 호출을하는게 좋을지(네트워크 호출? 코드 호출?), 레이어는 어디끼리 참조하고 있어야 좋을지 고민해보셔도 좋을거 같아요.
ex. member.application -> activity.domain?
ex. member.application -> activity.application?
| QuickStartRequest request = new QuickStartRequest(null, 12, 30, "오전", 300, Type.OFFLINE); | ||
| Set<ConstraintViolation<QuickStartRequest>> violations = validator.validate(request); | ||
|
|
||
| assertThat(violations).isNotEmpty(); | ||
| assertThat(violations).anyMatch(violation -> violation.getMessage().contains("이름은 필수 입력 항목입니다.")); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
given, when, then을 주석으로라도 명시해놓는게 추후 코드를 다시 읽거나 다른사람이 테스트 코드를 파악할때 도움이 됩니다!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
쓰다 안쓰다 했는데 확실하게 써주는게 좋겠네요!
감사합니다 ㅎㅎ
| private static final Pattern CONTENT_PREFIX_PATTERN = Pattern.compile(".*content :"); | ||
| private static final String LINE_SEPARATOR = "\n"; | ||
| private final RecommendationProvider recommendationProvider; | ||
| public List<ClovaRecommendationResponse> getRecommendationsFromClova(ClovaRecommendationRequest clovaRecommendationRequest) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
요기 함수위에 개행이 빠져있는것 같아요 ㅎㅎ
| @Service | ||
| @RequiredArgsConstructor | ||
| @Log4j2 | ||
| public class ClovaService { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service라는 네이밍은 통상적으로 어떤 역할을할때 사용되고 있을지 고민해봐도 좋을거 같습니다.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service 네이밍은 비즈니스 로직의 중심이 되는 계층이나 애플리케이션의 핵심 기능을 구현하고 관리하는 역할이라 알고있습니다.
코드를 작성하던 당시 추천 목록을 받아오니 핵심 기능을 구현한다 생각해 ClovaService라 지었습니다.
현재 저희 애플리케이션의 메인 로직은 추천 목록을 받아오는 것이 아닌, 받아온 추천 목록을 파싱해 사용자에게 전달하는 것이기에 Service가 아닌 RecommendationProvider로 이름을 변경했습니다.
더 깊게 생각할거리를 주셔서 감사합니다.
| return clovaStudioWebClient.post() | ||
| .uri(apiUrl) | ||
| .bodyValue(request) | ||
| .retrieve() | ||
| .bodyToMono(ClovaResponse.class) | ||
| .block(); | ||
| } catch (WebClientException e) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
여기서 block을 걸면 reactive의 이점을 누릴 수 있을까요?
reactive가 항상 빠른것은 아니며, 잘못사용하면 오히려 느려지는 경우가 많은데요. 가장 많이 하는 실수중 하나가, reactive 코드에서 blocking I/O를 유발하는 작업을 할때 인 것 같아요.
reactive가 어떻게 성능적인 이점을 주는지, 알아보셔도 좋을거 같아요.


개발파트 심사위원 코드리뷰용 PR입니다.
바쁜 와중에 코드 리뷰해 주셔서 정말 감사드립니다.